﻿using System.Collections;
using System.Collections.Generic;
using Devil;


#if UNITY_EDITOR
using UnityEditor;
using UnityEditorInternal;
using UnityEngine.Profiling;
#endif
using UnityEngine;

namespace GameToolkit
{
    public class MaterialCache
    {
        internal static readonly object _lock = new object();
        static readonly Dictionary<int, List<MaterialInst>> sAllInstances = new Dictionary<int, List<MaterialInst>>();
        static readonly CascadeList<MaterialInst> sReleaseInstances = new CascadeList<MaterialInst>(16);
        static readonly CascadeList<MaterialGroupOverrider> sDirtyOverriders = new CascadeList<MaterialGroupOverrider>(64);
        static int sReleaseCd;
#if UNITY_EDITOR
        static readonly string profiling = "Material Cache";
#endif
        public static int CachedMaterialsCount => sAllInstances.Count;
        public static void GetCachedMaterials(ICollection<IMaterialInstance> lst)
        {
            //var lst = CacheUtil.GetListCahce<IMaterialInstance>();
            lock (_lock)
            {
                foreach (var v in sAllInstances.Values)
                {
                    foreach (var mat in v)
                        lst.Add(mat);
                }
            }
            //return lst.GetEnumerator();
        }

        /// <summary>
        /// 创建由 shader-group 唯一对应的材质实例
        /// </summary>
        /// <param name="shader"></param>
        /// <param name="sharingGroup"></param>
        /// <returns></returns>
        public static IMaterialInstance GetMaterial(Shader shader, int sharingGroup = 0)
        {
            lock (_lock)
            {
                var srcId = shader.GetInstanceID();
                var id = MathExt.Hash(srcId, sharingGroup);
                List<MaterialInst> mats;
                if (!sAllInstances.TryGetValue(id, out mats))
                {
                    mats = new List<MaterialInst>(2);
                    sAllInstances.Add(id, mats);
                    //mat = new MaterialInst(id, shader);
                    //sAllInstances.Add(id, mat);
                }
                MaterialInst inst;
                for (int i = 0; i < mats.Count; i++)
                {
                    inst = mats[i];
                    if (inst.sourceId == srcId)
                        return inst;
                }
                inst = new MaterialInst(id, shader, sharingGroup);
                mats.Add(inst);
                return inst;
            }
        }

        /// <summary>
        /// 创建由 material-group 唯一对应的材质实例
        /// </summary>
        /// <param name="mat"></param>
        /// <param name="sharingGroup"></param>
        /// <returns></returns>
        public static IMaterialInstance GetMaterial(Material mat, int sharingGroup = 0)
        {
            var id = OverrideMaterialInstance.GetMatInstID(sharingGroup, mat);
            return GetMatInstance(id, mat, sharingGroup);
        }

        internal static MaterialInst GetMatInstance(int matId, Material src, int groupId)
        {
            lock (_lock)
            {
                List<MaterialInst> mats;
                if (!sAllInstances.TryGetValue(matId, out mats))
                {
                    mats = new List<MaterialInst>(2);
                    sAllInstances.Add(matId, mats);
                    //inst = new MaterialInst(matId, src);
                    //sAllInstances.Add(matId, inst);
                }
                var srcId = src.GetInstanceID();
                MaterialInst inst;
                for (int i = 0; i < mats.Count; i++)
                {
                    inst = mats[i];
                    if (inst.sourceId == srcId)
                        return inst;
                }
                inst = new MaterialInst(matId, src, groupId);
                mats.Add(inst);
                return inst;
            }
        }

        internal static bool IsDirty(MaterialGroupOverrider overrider)
        {
            lock (_lock)
            {
                return sDirtyOverriders.Contains(overrider);
            }
        }

        internal static void SetDirty(MaterialGroupOverrider overrider)
        {
            lock(_lock)
            {
                sDirtyOverriders.Add(overrider);
            }
        }

        static void RemoveInstance(MaterialInst inst)
        {
            List<MaterialInst> mats;
            if (sAllInstances.TryGetValue(inst.id, out mats))
            {
                mats.Remove(inst);
                if (mats.Count == 0)
                    sAllInstances.Remove(inst.id);
            }
        }

        // invoke from MainThread
        internal static void ExecuteUpdate()
        {
            lock (_lock)
            {
#if UNITY_EDITOR
                Profiler.BeginSample(profiling);
#endif
                for (int i = sDirtyOverriders.Count - 1; i >= 0; i--)
                {
                    var inst = sDirtyOverriders[i];
                    inst.ApplyDirtyBlocks();
                }
                sDirtyOverriders.Clear();
                // 为了减少同一个材质在相邻的若干帧中反复创建销毁，增加释放周期（每若干帧释放一次）
                if (sReleaseCd > 0)
                {
                    sReleaseCd--;
                }
                else
                {
                    sReleaseCd = 3;
                    for (int i = sReleaseInstances.Count - 1; i >= 0; i--)
                    {
                        var inst = sReleaseInstances[i];
                        if (inst.References == 0)
                        {
                            RemoveInstance(inst);
                            //sAllInstances.Remove(inst.id);
                            inst.Destroy();
                        }
                    }
                    sReleaseInstances.Clear();
                }
#if UNITY_EDITOR
                Profiler.EndSample();
#endif
            }
        }

        struct RendererID : System.IEquatable<RendererID>
        {
            int _id;
            Renderer _renderer;
            int _materialIndex;
            public RendererID(Renderer rend, int index)
            {
                _id = MathExt.Hash((object)rend == null ? 0 : rend.GetHashCode(), index);
                this._renderer = rend;
                this._materialIndex = index;
            }

            public void Apply(MaterialPropertyBlock block)
            {
                if (_renderer != null)
                    _renderer.SetPropertyBlock(block, _materialIndex);
            }

            public override int GetHashCode()
            {
                return _id;
            }

            public override bool Equals(object obj)
            {
                if (obj is RendererID rid)
                    return Equals(rid);
                else
                    return false;
            }

            public bool Equals(RendererID rid)
            {
                return this._renderer == rid._renderer && this._materialIndex == rid._materialIndex;
            }
        }

        internal class MaterialInst : MaterialGroupOverrider.Inst, IMaterialInstance
        {
            const byte FLAG_MATERIAL_PROPERTY = 1;
            const byte FLAG_MATERIAL_KEYWORD = 2;
            internal readonly static MaterialInst sharingInst = new MaterialInst();
            internal readonly int sourceId;
            internal readonly int groupId;
            internal readonly Material source;
            internal Material _material;
            public int GroupId => groupId;
            byte mDirtyFlags;
            Material IMaterialInstance.material => this._material;
            Material IMaterialInstance.sharingMaterial => source;

            //string assertName;
            //protected override string AssertName => assertName;
            //protected override bool AssertCondition => AssertName.Contains("Female01_m_eyebrow_01");

#if UNITY_EDITOR
            //bool mAsPrefab;
            //Renderer mMPBRenderer;
            //int mMatIndex;
            MaterialPropertyBlock mMPBforPrefab;
            HashSet<RendererID> mMPBSettings = new HashSet<RendererID>();
            internal void SetAsPrefab(bool isPrefab, Renderer r, int matIndex)
            {
                var rid = new RendererID(r, matIndex);
                bool dirty;
                if(isPrefab)
                {
                    dirty = mMPBSettings.Add(rid);
                    if (mMPBforPrefab == null)
                        mMPBforPrefab = new MaterialPropertyBlock();
                }
                else
                {
                    dirty = mMPBSettings.Remove(rid);
                }
                if (dirty && r != null)
                {
                    r.SetPropertyBlock(isPrefab ? mMPBforPrefab : null, matIndex);
                }
            }

            internal void FlushMPBValues()
            {
                foreach (var rid in mMPBSettings)
                {
                    rid.Apply(mMPBforPrefab);
                }
            }
#endif

            internal void Destroy()
            {
                if (_material != null)
                {
                    GlobalUtil.Destroy(_material);
                    _material = null;
                }
            }

            private MaterialInst() : base(0)
            {
                //assertName = "MaterialInst";
            }
            internal MaterialInst(int id, Material source, int groupId) : base(id)
            {
                this.groupId = groupId;
                this.sourceId = source.GetInstanceID();
                this.source = source;
                this._material = Object.Instantiate(source);
#if UNITY_EDITOR
                this._material.name = ParallelUtils.Concat(source.name, " (", id.ToString("X"), ")");
                this._material.hideFlags |= HideFlags.DontSave | HideFlags.NotEditable /*| HideFlags.HideInInspector*/;
                InternalEditorUtility.SetIsInspectorExpanded(this._material, false);
#endif
            }
            internal MaterialInst(int id, Shader shader, int grpId) : base(id)
            {
                this.groupId = grpId;
                this.sourceId = shader.GetInstanceID();
                this.source = null;
                this._material = new Material(shader);
#if UNITY_EDITOR
                this._material.name = ParallelUtils.Concat(shader.name, " (", id.ToString("X"), ")");
                this._material.hideFlags |= HideFlags.DontSave | HideFlags.NotEditable /*| HideFlags.HideInInspector*/;
                InternalEditorUtility.SetIsInspectorExpanded(this._material, false);
#endif
            }
            protected override void OnRelease()
            {
                sReleaseInstances.Add(this);
            }
            public override void EnableKeyword(string keyword)
            {
#if UNITY_EDITOR
                if (mMPBSettings.Count > 0 && source != null)
                    source.EnableKeyword(keyword);
#endif
                if (_material != null && !_material.IsKeywordEnabled(keyword))
                {
                    mDirtyFlags |= FLAG_MATERIAL_KEYWORD;
                    _material.EnableKeyword(keyword);
#if UNITY_EDITOR
                    //Debug.LogFormat("[{0}] {1} enable keyword: {2} ({3})", 
                    //    Time.frameCount, material.name, keyword, material.IsKeywordEnabled(keyword));
#endif
                }
            }
            public override void DisableKeyword(string keyword)
            {
#if UNITY_EDITOR
                if (mMPBSettings.Count > 0 && source != null)
                    source.DisableKeyword(keyword);
#endif
                if (_material != null && _material.IsKeywordEnabled(keyword))
                {
                    mDirtyFlags |= FLAG_MATERIAL_KEYWORD;
                    _material.DisableKeyword(keyword);
#if UNITY_EDITOR
                    //Debug.LogFormat("[{0}] {1} disable keyword: {2}", Time.frameCount, material.name, keyword);
#endif
                }
            }
            public override void Clear()
            {
#if UNITY_EDITOR
                if (mMPBforPrefab != null)
                    mMPBforPrefab.Clear();
#endif
                if (_material != null && source != null)
                {
                    if ((mDirtyFlags & FLAG_MATERIAL_PROPERTY) != 0)
                        _material.CopyPropertiesFromMaterial(source);
                    if ((mDirtyFlags & FLAG_MATERIAL_KEYWORD) != 0)
                        _material.enabledKeywords = source.enabledKeywords;
                    mDirtyFlags = 0;
                }
            }
            public override void SetColor(int id, Color color)
            {
#if UNITY_EDITOR
                if (mMPBforPrefab != null)
                    mMPBforPrefab.SetColor(id, color);
#endif
                _material.SetColor(id, color);
                mDirtyFlags |= FLAG_MATERIAL_PROPERTY;
            }
            public override void SetInt(int id, int value)
            {
#if UNITY_EDITOR
                if (mMPBforPrefab != null)
                    mMPBforPrefab.SetInt(id, value);
#endif
                _material.SetInt(id, value);
                mDirtyFlags |= FLAG_MATERIAL_PROPERTY;
            }
            public override void SetFloat(int id, float value)
            {
#if UNITY_EDITOR
                if (mMPBforPrefab != null)
                    mMPBforPrefab.SetFloat(id, value);
#endif
                _material.SetFloat(id, value);
                mDirtyFlags |= FLAG_MATERIAL_PROPERTY;
            }

            public override void SetVector(int id, Vector4 vector)
            {
#if UNITY_EDITOR
                if (mMPBforPrefab != null)
                    mMPBforPrefab.SetVector(id, vector);
#endif
                _material.SetVector(id, vector);
                mDirtyFlags |= FLAG_MATERIAL_PROPERTY;
            }

            public override void SetTexture(int id, Texture tex)
            {
#if UNITY_EDITOR
                if (mMPBforPrefab != null)
                    mMPBforPrefab.SetTexture(id, tex);
#endif
                _material.SetTexture(id, tex);
                mDirtyFlags |= FLAG_MATERIAL_PROPERTY;
            }

            public override string ToString()
            {
                string title;
                if (source == null)
                    title = _material == null ? "[NULL]" : _material.shader.name;
#if UNITY_EDITOR
                else if (AssetDatabase.Contains(source))
                {
                    title = AssetDatabase.GetAssetPath(source);
                    if (string.IsNullOrEmpty(title))
                        title = source.name;
                }
#endif
                else
                    title = source.name;
                return string.Format("material: {0} (refs: {1})", title, References);
            }
        }

    }
}
